/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.replica.replicaisland;

import java.util.Comparator;

/**
 * Handles collision against the background. Snaps colliding objects out of
 * collision and reports the hit to the parent game object.
 */
public class BackgroundCollisionComponent extends GameComponent {
	private Vector2 mPreviousPosition;
	private int mWidth;
	private int mHeight;
	private int mHorizontalOffset;
	private int mVerticalOffset;

	// Workspace vectors. Allocated up front for speed.
	private Vector2 mCurrentPosition;
	private Vector2 mPreviousCenter;
	private Vector2 mDelta;
	private Vector2 mFilterDirection;
	private Vector2 mHorizontalHitPoint;
	private Vector2 mHorizontalHitNormal;
	private Vector2 mVerticalHitPoint;
	private Vector2 mVerticalHitNormal;
	private Vector2 mRayStart;
	private Vector2 mRayEnd;
	private Vector2 mTestPointStart;
	private Vector2 mTestPointEnd;
	private Vector2 mMergedNormal;

	/**
	 * Sets up the collision bounding box. This box may be a different size than
	 * the bounds of the sprite that this object controls.
	 * 
	 * @param width
	 *            The width of the collision box.
	 * @param height
	 *            The height of the collision box.
	 * @param horzOffset
	 *            The offset of the collision box from the object's origin in
	 *            the x axis.
	 * @param vertOffset
	 *            The offset of the collision box from the object's origin in
	 *            the y axis.
	 */
	public BackgroundCollisionComponent(int width, int height, int horzOffset,
			int vertOffset) {
		super();
		setPhase(ComponentPhases.COLLISION_RESPONSE.ordinal());
		mPreviousPosition = new Vector2();
		mWidth = width;
		mHeight = height;
		mHorizontalOffset = horzOffset;
		mVerticalOffset = vertOffset;

		mCurrentPosition = new Vector2();
		mPreviousCenter = new Vector2();
		mDelta = new Vector2();
		mFilterDirection = new Vector2();
		mHorizontalHitPoint = new Vector2();
		mHorizontalHitNormal = new Vector2();
		mVerticalHitPoint = new Vector2();
		mVerticalHitNormal = new Vector2();
		mRayStart = new Vector2();
		mRayEnd = new Vector2();
		mTestPointStart = new Vector2();
		mTestPointEnd = new Vector2();
		mMergedNormal = new Vector2();
	}

	public BackgroundCollisionComponent() {
		super();
		setPhase(ComponentPhases.COLLISION_RESPONSE.ordinal());
		mPreviousPosition = new Vector2();
		mCurrentPosition = new Vector2();
		mPreviousCenter = new Vector2();
		mDelta = new Vector2();
		mFilterDirection = new Vector2();
		mHorizontalHitPoint = new Vector2();
		mHorizontalHitNormal = new Vector2();
		mVerticalHitPoint = new Vector2();
		mVerticalHitNormal = new Vector2();
		mRayStart = new Vector2();
		mRayEnd = new Vector2();
		mTestPointStart = new Vector2();
		mTestPointEnd = new Vector2();
		mMergedNormal = new Vector2();
	}

	@Override
	public void reset() {
		mPreviousPosition.zero();
	}

	public void setSize(int width, int height) {
		mWidth = width;
		mHeight = height;
		// TODO: Resize might cause new collisions.
	}

	public void setOffset(int horzOffset, int vertOffset) {
		mHorizontalOffset = horzOffset;
		mVerticalOffset = vertOffset;
	}

	/**
	 * This function is the meat of the collision response logic. Our collision
	 * detection and response must be capable of dealing with arbitrary surfaces
	 * and must be frame rate independent (we must sweep the space in-between
	 * frames to find collisions reliably). The following algorithm is used to
	 * keep the collision box out of the collision world. 1. Cast a ray from the
	 * center point of the box at its position last frame to the edge of the box
	 * at its current position. If the ray intersects anything, snap the box
	 * back to the point of intersection. 2. Perform Step 1 twice: once looking
	 * for surfaces opposing horizontal movement and again for surfaces opposing
	 * vertical movement. These two ray tests approximate the movement of the
	 * box between the previous frame and this one. 3. Since most collisions are
	 * collisions with the ground, more precision is required for vertical
	 * intersections. Perform another ray test, this time from the top of the
	 * box's position (after snapping in Step 2) to the bottom. Snap out of any
	 * vertical surfaces that the ray encounters. This will ensure consistent
	 * snapping behavior on incline surfaces. 4. Add the normals of the surfaces
	 * that were hit up and normalize the result to produce a direction
	 * describing the average slope of the surfaces that the box is resting on.
	 * Physics will use this value as a normal to resolve collisions with the
	 * background.
	 */
	@Override
	public void update(float timeDelta, BaseObject parent) {
		GameObject parentObject = (GameObject) parent;
		parentObject.setBackgroundCollisionNormal(Vector2.ZERO);
		if (mPreviousPosition.length2() != 0) {
			CollisionSystem collision = sSystemRegistry.collisionSystem;
			if (collision != null) {
				final int left = mHorizontalOffset;
				final int bottom = mVerticalOffset;
				final int right = left + mWidth;
				final int top = bottom + mHeight;
				final float centerOffsetX = ((mWidth) / 2.0f) + left;
				final float centerOffsetY = ((mHeight) / 2.0f) + bottom;

				mCurrentPosition.set(parentObject.getPosition());
				mDelta.set(mCurrentPosition);
				mDelta.subtract(mPreviousPosition);

				mPreviousCenter.set(centerOffsetX, centerOffsetY);
				mPreviousCenter.add(mPreviousPosition);

				boolean horizontalHit = false;
				boolean verticalHit = false;

				mVerticalHitPoint.zero();
				mVerticalHitNormal.zero();
				mHorizontalHitPoint.zero();
				mHorizontalHitNormal.zero();

				// The order in which we sweep the horizontal and vertical space
				// can affect the
				// final result because we perform incremental snapping
				// mid-sweep. So it is
				// necessary to sweep in the primary direction of movement
				// first.
				if (Math.abs(mDelta.x) > Math.abs(mDelta.y)) {
					horizontalHit = sweepHorizontal(mPreviousCenter,
							mCurrentPosition, mDelta, left, right,
							centerOffsetY, mHorizontalHitPoint,
							mHorizontalHitNormal, parentObject);
					verticalHit = sweepVertical(mPreviousCenter,
							mCurrentPosition, mDelta, bottom, top,
							centerOffsetX, mVerticalHitPoint,
							mVerticalHitNormal, parentObject);

				} else {
					verticalHit = sweepVertical(mPreviousCenter,
							mCurrentPosition, mDelta, bottom, top,
							centerOffsetX, mVerticalHitPoint,
							mVerticalHitNormal, parentObject);
					horizontalHit = sweepHorizontal(mPreviousCenter,
							mCurrentPosition, mDelta, left, right,
							centerOffsetY, mHorizontalHitPoint,
							mHorizontalHitNormal, parentObject);
				}

				// force the collision volume to stay within the bounds of the
				// world.
				LevelSystem level = sSystemRegistry.levelSystem;
				if (level != null) {
					if (mCurrentPosition.x + left < 0.0f) {
						mCurrentPosition.x = (-left + 1);
						horizontalHit = true;
						mHorizontalHitNormal.x = (mHorizontalHitNormal.x + 1.0f);
						mHorizontalHitNormal.normalize();
					} else if (mCurrentPosition.x + right > level
							.getLevelWidth()) {
						mCurrentPosition.x = (level.getLevelWidth() - right - 1);
						mHorizontalHitNormal.x = (mHorizontalHitNormal.x - 1.0f);
						mHorizontalHitNormal.normalize();
						horizontalHit = true;
					}

					/*
					 * if (mCurrentPosition.y + bottom < 0.0f) {
					 * mCurrentPosition.y = (-bottom + 1); verticalHit = true;
					 * mVerticalHitNormal.y = (mVerticalHitNormal.y + 1.0f);
					 * mVerticalHitNormal.normalize(); } else
					 */if (mCurrentPosition.y + top > level.getLevelHeight()) {
						mCurrentPosition.y = (level.getLevelHeight() - top - 1);
						mVerticalHitNormal.y = (mVerticalHitNormal.y - 1.0f);
						mVerticalHitNormal.normalize();
						verticalHit = true;
					}

				}

				// One more set of tests to make sure that we are aligned with
				// the surface.
				// This time we will just check the inside of the bounding box
				// for intersections.
				// The sweep tests above will keep us out of collision in most
				// cases, but this
				// test will ensure that we are aligned to incline surfaces
				// correctly.

				// Shoot a vertical line through the middle of the box.
				if (mDelta.x != 0.0f && mDelta.y != 0.0f) {
					float yStart = top;
					float yEnd = bottom;

					mRayStart.set(centerOffsetX, yStart);
					mRayStart.add(mCurrentPosition);

					mRayEnd.set(centerOffsetX, yEnd);
					mRayEnd.add(mCurrentPosition);

					mFilterDirection.set(mDelta);

					if (collision
							.castRay(mRayStart, mRayEnd, mFilterDirection,
									mVerticalHitPoint, mVerticalHitNormal,
									parentObject)) {

						// If we found a collision, use this surface as our
						// vertical intersection
						// for this frame, even if the sweep above also found
						// something.
						verticalHit = true;
						// snap
						if (mVerticalHitNormal.y > 0.0f) {
							mCurrentPosition.y = (mVerticalHitPoint.y - bottom);
						} else if (mVerticalHitNormal.y < 0.0f) {
							mCurrentPosition.y = (mVerticalHitPoint.y - top);
						}
					}

					// Now the horizontal version of the same test
					float xStart = left;
					float xEnd = right;
					if (mDelta.x < 0.0f) {
						xStart = right;
						xEnd = left;
					}

					mRayStart.set(xStart, centerOffsetY);
					mRayStart.add(mCurrentPosition);

					mRayEnd.set(xEnd, centerOffsetY);
					mRayEnd.add(mCurrentPosition);

					mFilterDirection.set(mDelta);

					if (collision.castRay(mRayStart, mRayEnd, mFilterDirection,
							mHorizontalHitPoint, mHorizontalHitNormal,
							parentObject)) {

						// If we found a collision, use this surface as our
						// horizontal intersection
						// for this frame, even if the sweep above also found
						// something.
						horizontalHit = true;
						// snap
						if (mHorizontalHitNormal.x > 0.0f) {
							mCurrentPosition.x = (mHorizontalHitPoint.x - left);
						} else if (mHorizontalHitNormal.x < 0.0f) {
							mCurrentPosition.x = (mHorizontalHitPoint.x - right);
						}
					}
				}

				// Record the intersection for other systems to use.
				final TimeSystem timeSystem = sSystemRegistry.timeSystem;

				if (timeSystem != null) {
					float time = timeSystem.getGameTime();
					if (horizontalHit) {
						if (mHorizontalHitNormal.x > 0.0f) {
							parentObject.setLastTouchedLeftWallTime(time);
						} else {
							parentObject.setLastTouchedRightWallTime(time);
						}
						// parentObject.setBackgroundCollisionNormal(mHorizontalHitNormal);
					}

					if (verticalHit) {
						if (mVerticalHitNormal.y > 0.0f) {
							parentObject.setLastTouchedFloorTime(time);
						} else {
							parentObject.setLastTouchedCeilingTime(time);
						}
						// parentObject.setBackgroundCollisionNormal(mVerticalHitNormal);
					}

					// If we hit multiple surfaces, merge their normals together
					// to produce an
					// average direction of obstruction.
					if (true) { // (verticalHit && horizontalHit) {
						mMergedNormal.set(mVerticalHitNormal);
						mMergedNormal.add(mHorizontalHitNormal);
						mMergedNormal.normalize();
						parentObject
								.setBackgroundCollisionNormal(mMergedNormal);
					}

					parentObject.setPosition(mCurrentPosition);
				}

			}
		}
		mPreviousPosition.set(parentObject.getPosition());
	}

	/*
	 * Sweeps the space between two points looking for surfaces that oppose
	 * horizontal movement.
	 */
	protected boolean sweepHorizontal(Vector2 previousPosition,
			Vector2 currentPosition, Vector2 delta, int left, int right,
			float centerY, Vector2 hitPoint, Vector2 hitNormal,
			GameObject parentObject) {
		boolean hit = false;
		if (!Utils.close(delta.x, 0.0f)) {
			CollisionSystem collision = sSystemRegistry.collisionSystem;

			// Shoot a ray from the center of the previous frame's box to the
			// edge (left or right,
			// depending on the direction of movement) of the current box.
			mTestPointStart.y = (centerY);
			mTestPointStart.x = (left);
			int offset = -left;
			if (delta.x > 0.0f) {
				mTestPointStart.x = (right);
				offset = -right;
			}

			// Filter out surfaces that do not oppose motion in the horizontal
			// direction, or
			// push in the same direction as movement.
			mFilterDirection.set(delta);
			mFilterDirection.y = (0);

			mTestPointEnd.set(currentPosition);
			mTestPointEnd.add(mTestPointStart);
			if (collision.castRay(previousPosition, mTestPointEnd,
					mFilterDirection, hitPoint, hitNormal, parentObject)) {
				// snap
				currentPosition.x = (hitPoint.x + offset);
				hit = true;
			}
		}
		return hit;
	}

	/*
	 * Sweeps the space between two points looking for surfaces that oppose
	 * vertical movement.
	 */
	protected boolean sweepVertical(Vector2 previousPosition,
			Vector2 currentPosition, Vector2 delta, int bottom, int top,
			float centerX, Vector2 hitPoint, Vector2 hitNormal,
			GameObject parentObject) {
		boolean hit = false;
		if (!Utils.close(delta.y, 0.0f)) {
			CollisionSystem collision = sSystemRegistry.collisionSystem;
			// Shoot a ray from the center of the previous frame's box to the
			// edge (top or bottom,
			// depending on the direction of movement) of the current box.
			mTestPointStart.x = (centerX);
			mTestPointStart.y = (bottom);
			int offset = -bottom;
			if (delta.y > 0.0f) {
				mTestPointStart.y = (top);
				offset = -top;
			}

			mFilterDirection.set(delta);
			mFilterDirection.x = (0);

			mTestPointEnd.set(currentPosition);
			mTestPointEnd.add(mTestPointStart);
			if (collision.castRay(previousPosition, mTestPointEnd,
					mFilterDirection, hitPoint, hitNormal, parentObject)) {
				hit = true;
				// snap
				currentPosition.y = (hitPoint.y + offset);
			}

		}
		return hit;
	}

	/** Comparator for hit points. */
	private static class HitPointDistanceComparator implements
			Comparator<HitPoint> {
		private Vector2 mOrigin;

		public HitPointDistanceComparator() {
			super();
			mOrigin = new Vector2();
		}

		public final void setOrigin(Vector2 origin) {
			mOrigin.set(origin);
		}

		public final void setOrigin(float x, float y) {
			mOrigin.set(x, y);
		}

		public int compare(HitPoint object1, HitPoint object2) {
			int result = 0;
			if (object1 != null && object2 != null) {
				final float obj1Distance = object1.hitPoint.distance2(mOrigin);
				final float obj2Distance = object2.hitPoint.distance2(mOrigin);
				final float distanceDelta = obj1Distance - obj2Distance;
				result = distanceDelta < 0.0f ? -1 : 1;
			} else if (object1 == null && object2 != null) {
				result = 1;
			} else if (object2 == null && object1 != null) {
				result = -1;
			}
			return result;
		}
	}
}
